﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using SilverMindMap.Core.Model;

namespace SilverMindMap.Core.View
{
    public class CircleLayoutProvider : IDisposable
    {
        private const double RadiusMargin = 20.0;

        private bool _disposed = false;
        private readonly MindMap _mindMap;
        private readonly MindMapObserver _mindMapObserver;
        private readonly INodeSizeProvider _sizeProvider;
        private readonly INodeAnimator _nodeAnimator;

        public CircleLayoutProvider(MindMap mindMap, INodeSizeProvider sizeProvider, INodeAnimator nodeAnimator)
        {
            Guard.ArgumentNotNull(mindMap, "mindMap");
            Guard.ArgumentNotNull(sizeProvider, "sizeProvider");
            Guard.ArgumentNotNull(nodeAnimator, "nodeAnimator");

            _mindMap = mindMap;
            _sizeProvider = sizeProvider;
            _nodeAnimator = nodeAnimator;

            _mindMapObserver = new MindMapObserver(_mindMap);
            _mindMapObserver.NodeAdded += (s, a) => RefreshLayout();
            _mindMapObserver.NodeRemoved += (s, a) => RefreshLayout();
            _sizeProvider.NodeSizeChanged += OnNodeSizeChanged;
            _sizeProvider.NodesSizesInvalidated += OnNodesSizesInvalidated;
            _mindMap.PropertyChanged += OnMindMapPropertyChanged;
        }

        private void OnNodeSizeChanged(object sender, NodeSizeChangedEventArgs e)
        {
            RefreshLayout();
        }

        private void OnNodesSizesInvalidated(object sender, EventArgs e)
        {
            RefreshLayout();
        }

        private void OnMindMapPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "RootNode")
                RefreshLayout();
        }

        public void Dispose()
        {
            if (_disposed)
                return;

            _mindMapObserver.Dispose();
            _sizeProvider.NodeSizeChanged -= OnNodeSizeChanged;
            _sizeProvider.NodesSizesInvalidated -= OnNodesSizesInvalidated;
            _mindMap.PropertyChanged -= OnMindMapPropertyChanged;

            _disposed = true;
        }

        private void CheckNotDisposed()
        {
            if (_disposed)
                throw new ObjectDisposedException("CircleLayoutProvider");
        }

        private void RefreshLayout()
        {
            CheckNotDisposed();

            var nodeLevelMap = AssignLevelsToNodes();
            var levelNodeMap = AssignNodesToLevels(nodeLevelMap);

            if (nodeLevelMap.Count == 0)
                return;

            FillChildrenOnLevels(nodeLevelMap[_mindMap.RootNode]);

            _nodeAnimator.AnimateNodeTo(_mindMap.RootNode, new Point(0, 0));

            LayoutChildren(new Point(0, 0), nodeLevelMap[_mindMap.RootNode], 0.0, 2 * Math.PI);
        }

        private void LayoutChildren(Point currentNodePosition, NodeLevelInfo currentNode, double startAngle, double endAngle)
        {
            if (currentNode.Children.Count == 0)
                return;

            double sumMaxChildrenOnLevel = currentNode.Children.Select(c => c.MaximumChildrenOnSingleLevel).Sum();

            double currentNodeRadius = GetNodeRadius(currentNode.Node);
            
            //double angleDelta = (endAngle - startAngle) / Math.Max(2, currentNode.Children.Count);
            // double currentAngle = startAngle + angleDelta / 2.0;
            double currentStartAngle = startAngle;

            double finalRadius = currentNodeRadius + RadiusMargin;
            double previousChildRadius = 0.0;

            foreach (var child in currentNode.Children)
            {
                double angleDelta = (endAngle - startAngle) * child.MaximumChildrenOnSingleLevel / sumMaxChildrenOnLevel;
                if (angleDelta > Math.PI)
                    angleDelta = Math.PI;

                double childRadius = GetNodeRadius(child.Node);
                double minimumAngleNeeded = 2 * Math.Asin(Math.Max(childRadius, previousChildRadius) / finalRadius);
                if (minimumAngleNeeded > angleDelta || child == currentNode.Children[0])
                    finalRadius += childRadius + RadiusMargin;
                previousChildRadius = childRadius;

                double x = Math.Sin(currentStartAngle + angleDelta / 2.0) * finalRadius + currentNodePosition.X;
                double y = Math.Cos(currentStartAngle + angleDelta / 2.0) * finalRadius + currentNodePosition.Y;
                var point = new Point(x, y);
                _nodeAnimator.AnimateNodeTo(child.Node, new Point(x, y));

                LayoutChildren(point, child, currentStartAngle, currentStartAngle + angleDelta);

                currentStartAngle += angleDelta;
            }
        }

        private double GetNodeRadius(Node node)
        {
            var size = _sizeProvider.GetNodeSize(node);
            return Math.Sqrt(size.Width * size.Width + size.Height * size.Height) / 2.0;
        }

        private IDictionary<Node, NodeLevelInfo> AssignLevelsToNodes()
        {
            var result = new Dictionary<Node, NodeLevelInfo>();
            
            var queue = new Queue<NodeLevelInfo>();
            if (_mindMap.RootNode != null)
            {
                var rootLevelInfo = new NodeLevelInfo(_mindMap.RootNode, 0);
                queue.Enqueue(rootLevelInfo);
                result[_mindMap.RootNode] = rootLevelInfo;
            }

            while (queue.Count > 0)
            {
                var currentNode = queue.Dequeue();

                foreach (var link in currentNode.Node.Links)
                {
                    var linkedNode = (link.FirstNode == currentNode.Node ? link.SecondNode : link.FirstNode);
                    if (result.ContainsKey(linkedNode))
                        continue;

                    var linkedNodeInfo = new NodeLevelInfo(linkedNode, currentNode.Level + 1);
                    queue.Enqueue(linkedNodeInfo);
                    result[linkedNode] = linkedNodeInfo;

                    currentNode.Children.Add(linkedNodeInfo);
                }
            }

            return result;
        }

        private void FillChildrenOnLevels(NodeLevelInfo rootNode)
        {
            rootNode.ChildrenOnLevels.Clear();

            int maxChildLevel = 0;
            foreach (var child in rootNode.Children)
            { 
                FillChildrenOnLevels(child);
                maxChildLevel = Math.Max(child.ChildrenOnLevels.Count, maxChildLevel);
            }

            // This level
            rootNode.ChildrenOnLevels.Add(1);

            rootNode.ChildrenOnLevels.Add(rootNode.Children.Count);

            for (int i = 0; i < maxChildLevel; i++)
            {
                int sum = 0;
                foreach (var child in rootNode.Children)
                {
                    if (child.ChildrenOnLevels.Count > i)
                        sum += child.ChildrenOnLevels[i];
                }

                rootNode.ChildrenOnLevels.Add(sum);
            }

            rootNode.MaximumChildrenOnSingleLevel = rootNode.ChildrenOnLevels.Max();
        }

        private IList<IList<NodeLevelInfo>> AssignNodesToLevels(IDictionary<Node, NodeLevelInfo> nodeLevelMap)
        {
            var result = new List<IList<NodeLevelInfo>>();
            
            foreach (var nodeLevel in nodeLevelMap)
            {
                while (nodeLevel.Value.Level + 1 > result.Count)
                    result.Add(new List<NodeLevelInfo>());

                result[nodeLevel.Value.Level].Add(nodeLevel.Value);
            }

            return result;
        }

        private class NodeLevelInfo
        {
            public readonly Node Node;
            public readonly int Level;
            public readonly IList<NodeLevelInfo> Children; 
            public readonly IList<int> ChildrenOnLevels;
            public int MaximumChildrenOnSingleLevel = 0;

            public NodeLevelInfo(Node node, int level)
            {
                this.Node = node;
                this.Level = level;
                this.Children = new List<NodeLevelInfo>();
                this.ChildrenOnLevels = new List<int>();
            }
        }
    }
}
